/*
 * slice2cube: Convert slices of TIFF images to cubes.
 * GOU Lingfeng <goulf@ion.ac.cn>
 *
 * Only TIFF files are allowed as input.
 * Produce NRRD output files.
 *
 * 1/2/4-bit images are converted to 8-bit.
 * 8-bit images are preserved as 8-bit.
 * 16-bit images can be converted to 8-bit or preserved as 16-bit.
 * Images with other bits/sample value are not supported.
 *
 */

#include <iostream>
#include <fstream>

#include <getopt.h>
#include <math.h>

#include "slice2cube.h"
#include "public/utils.h"
#include "config-fnt.h"

/* Options */
static const char* opts=":i:r:c:d:DMo:j::B:S::C8::Yhv";
static const struct option opts_long[]=
{
	/* Mostly used */
	{"input-list", 1, nullptr, 'i'},
	{"resolution", 1, nullptr, 'r'},
	{"cube-size", 1, nullptr, 'c'},
	{"downsample-factor", 1, nullptr, 'd'},
	{"downsample-only", 0, nullptr, 'D'},
	{"max-intensity", 0, nullptr, 'M'},
	{"output-directory", 1, nullptr, 'o'},
	{"jobs", 2, nullptr, 'j'},
	/* Rarely used */
	{"buffer-size", 1, nullptr, 'B'},
	{"skip-check", 2, nullptr, 'S'},
	{"continue", 0, nullptr, 'C'},
	{"8bit", 2, nullptr, '8'},
	{"yes", 0, nullptr, 'Y'},
	/* Must-have */
	{"help", 0, nullptr, 'h'},
	{"version", 0, nullptr, 'v'},
	{nullptr, 0, nullptr, 0}
};

/* Print version information and exit */
void version(const char* name) {
	std::cout<<name<<" (" PACKAGE_NAME_LONG ") " PACKAGE_VERSION "\n";
	std::cout<<"Copyright (C) 2015-2016 " PACKAGE_ORG "\n";
	std::cout<<"License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n\n";
	std::cout<<"Written by: " PACKAGE_AUTHORS "\n";
	exit(0);
}
/* Print usage information and exit */
void usage(const char* name, int ecode) {
	if(ecode) {
		std::cerr<<"Try '"<<name<<" --help' for more information.\n";
		exit(ecode);
	}
	std::cout<<"Usage: "<<name<<" [OPTION]... [TIFF_FILE]...\n";
	std::cout<<
		"Convert image slices in TIFF_FILEs to cubes.\n\n"
		"Options:\n"
		"  -i, --input-list=FILE                    [OPTIONAL]\n"
		"         Convert files listed in FILE (before files specified in\n"
		"         command line).\n"
		"  -r, --resolution=XRES:YRES:ZRES          [REQUIRED]\n"
		"         Specify image resolution in each direction (in µm/voxel).\n"
		"  -c, --cube-size=WIDTH:HEIGHT:DEPTH       [OPTIONAL]\n"
		"         Convert to cubes of the given size (in voxel). WIDTH and\n"
		"         HEIGHT must be multiples of 8. Approximately equal physical\n"
		"         edge lengths are prefered.\n"
		"  -d, --downsample-factor=DSX:DSY:DSZ      [OPTIONAL]\n"
		"         Use these factors to generate downsampled data. If `1:1:1'\n"
		"         is given, slices are put into a single cube with original\n"
		"         resolution.\n"
		"  -D, --downsample-only                    [OPTIONAL]\n"
		"  -M, --max-intensity                      [OPTIONAL]\n"
		"  -o, --output-directory=DIR               [OPTIONAL]\n"
		"         Write output files to DIR. Current working directory is used\n"
		"         by default.\n"
		"  -j, --jobs[=N]                           [OPTIONAL]\n"
		"         Run N jobs in parallel. Use all the CPUs if N is not given.\n\n"
		"  -B, --buffer-size=SIZE                   [OPTIONAL]\n"
		"         Limit memory usage to SIZE (in GiB). The default behavior\n"
		"         is to use half of physical memory.\n"
		"  -S, --skip-check[=DEPTH_HINT]            [OPTIONAL]\n"
		"         Assume correctness of input images and skip checking. Since\n"
		"         a TIFF file can contain more than one slices, total number\n"
		"         of slices is not known. DEPTH_HINT is used as a guess. If it"
		"         is not given, assume each file contains one slice.\n"
		"  -C, --continue                           [OPTIONAL]\n"
		"         Continue previous operation.\n"
		"  -8, --8bit[=FROM:TO]                     [OPTIONAL]\n"
		"         Convert 16bit input to 8bit. Intensity values <=FROM will be\n"
		"         mapped to 0; those >=TO, mapped to 255. Linear interpolation\n"
		"         is used for values in (FROM, TO). 0:65535 is used by default.\n"
		"  -Y, --yes                                [OPTIONAL]\n"
		"         Skip confirmation.\n\n"
		"  -h, --help       [OPTIONAL] Print this help, and do nothing else.\n"
		"  -v, --version    [OPTIONAL] Print version information and exit.\n\n"
		"Use 'BLACK' or 'WHITE' in input file list or command line to produce\n"
		"a blank slice.\n\n"
		"Report bugs to <" PACKAGE_BUGREPORT ">\n"
		PACKAGE_NAME_LONG " home page: <" PACKAGE_URL ">\n";
	exit(ecode);
}

bool get_confirmation() {
	std::cerr<<"Continue? [Y/N]: ";
	std::string l;
	while(std::getline(std::cin, l)) {
		if(l[0]=='y' || l[0]=='Y')
			return true;
		if(l[0]=='n' || l[0]=='N')
			return false;
		std::cerr<<"Continue? [Y/N]: ";
	}
	return false;
}

int main(int argc, char* argv[]) {

	const char* input_file_list=nullptr;
	std::string outdir=".";
	int ncpu=1;

	double xres=0;
	double yres=0;
	double zres=0;

	uint64_t xsize=0;
	uint64_t ysize=0;
	uint64_t zsize=0;

	uint32_t dsx=0;
	uint32_t dsy=0;
	uint32_t dsz=0;

	bool ds_only=false;
	bool ds_mip=false;

	double cachesizef=0; // XXX

	uint64_t depth_hint=0;
	bool skip_check=false;

	bool resume=false;

	uint32_t conv_x0=2, conv_x1=0;

	bool yes_set=false;

	int opt;
	int prev_optind=optind;
	while((opt=getopt_long(argc, argv, opts, opts_long, nullptr))!=-1) {
		try {
			switch(opt) {
				case '?':
					throwError("unrecognized option '", argv[prev_optind], "'\n");
				case ':':
					throwError("option '", argv[prev_optind], "' requires an argument\n");
				case 'h':
					usage(argv[0], 0);
				case 'v':
					version(argv[0]);
				case 'i':
					input_file_list=optarg;
					break;
				case 'r':
					if(!parseTuple(optarg, &xres, &yres, &zres))
						throwError("invalid argument for '", argv[prev_optind], "'\nPlease give 3 floating-point numbers separated by ':'.\n");
					if(xres<=0 || yres<=0 || zres<=0)
						throwError("invalid argument for '", argv[prev_optind], "'\nNeed positive numbers.\n");
					break;
				case 'c':
					if(!parseTuple(optarg, &xsize, &ysize, &zsize))
						throwError("invalid argument for '", argv[prev_optind], "'\nPlease give 3 integers separated by ':'.\n");
					if(xsize<=0 || ysize<=0 || zsize<=0)
						throwError("invalid argument for '", argv[prev_optind], "'\nNeed positive integers.\n");
					if(xsize%8!=0 || ysize%8!=0)
						throwError("invalid argument for '", argv[prev_optind], "'\nWidth and height must be multiples of 8.\n");
					break;
				case 'd':
					if(!parseTuple(optarg, &dsx, &dsy, &dsz))
						throwError("invalid argument for '", argv[prev_optind], "'.\nPlease give 3 integers separated by ':'.\n");
					if(dsx<=0 || dsy<=0 || dsz<=0)
						throwError("invalid argument for '", argv[prev_optind], "'.\nNeed positive integers.\n");
					break;
				case 'D':
					ds_only=true;
					break;
				case 'M':
					ds_mip=true;
					break;
				case 'o':
					outdir=optarg;
					break;
				case 'j':
					if(optarg) {
						if(!parseTuple<int>(optarg, &ncpu))
							throwError("invalid argument for '", argv[prev_optind], "'.\nPlease give an integer.\n");
						if(ncpu<=0)
							throwError("invalid argument for '", argv[prev_optind], "'.\nNeed a positive integer.\n");
					} else {
						ncpu=getNumberOfCpus();
						if(ncpu<1)
							ncpu=1;
					}
					break;
				case 'B':
					if(!parseTuple<double>(optarg, &cachesizef))
						throwError("invalid argument for '", argv[prev_optind], "'.\nPlease give a floating-point number.\n");
					if(cachesizef<=0)
						throwError("invalid argument for '", argv[prev_optind], "'.\nNeed a positive number.\n");
					break;
				case 'S':
					if(optarg) {
						if(!parseTuple<uint64_t>(optarg, &depth_hint))
							throwError("invalid argument for '", argv[prev_optind], "'.\nPlease give an integer.\n");
						if(depth_hint<=0)
							throwError("invalid argument for '", argv[prev_optind], "'.\nNeed a positive integer.\n");
					} else {
						depth_hint=0;
					}
					skip_check=true;
					break;
				case 'C':
					resume=true;
					break;
				case '8':
					if(optarg) {
						if(!parseTuple(optarg, &conv_x0, &conv_x1))
							throwError("invalid argument for '", argv[prev_optind], "'.\nPlease give 2 integers.\n");
						if(conv_x0<0 || conv_x0>65535 || conv_x1<0 || conv_x1>65535)
							throwError("invalid argument for '", argv[prev_optind], "'.\nNeed integers in the range [0, 65536).\n");
						if(conv_x0>=conv_x1)
							throwError("invalid argument for '", argv[prev_optind], "'.\nThe first number should be smaller.\n");
					} else {
						conv_x0=0;
						conv_x1=65535;
					}
					break;
				case 'Y':
					yes_set=true;
					break;
				default:
					throwError("Unexpected error.\n");
			}
		} catch(std::exception& e) {
			std::cerr<<argv[0]<<": "<<e.what();
			usage(argv[0], -1);
		}
		prev_optind=optind;
	}

	std::vector<std::string> inputfiles;
	try {
		if(xres<=0)
			throwError("resolution not specified.\n");

		if(input_file_list) {
			std::ifstream fs{input_file_list};
			if(!fs)
				throwError("Failed to open input file list.\n");
			std::string l;
			while(std::getline(fs, l)) {
				inputfiles.push_back(l);
			}
			if(!fs.eof())
				throwError("Failed to read line.\n");
		}
		for(int i=optind; i<argc; i++) {
			inputfiles.emplace_back(argv[i]);
		}

		if(inputfiles.empty())
			throwError("No input files specified.\n");
		if(inputfiles[0]=="BLACK" || inputfiles[0]=="WHITE"
				|| inputfiles.back()=="BLACK" || inputfiles.back()=="WHITE")
			throwError("The first and last slices must not be BLACK or WHITE.\n");

		createDirectory(outdir);
	} catch(std::exception& e) {
		std::cerr<<argv[0]<<": "<<e.what();
		usage(argv[0], -1);
	}

	uint64_t width, height, depth;
	int channels;
	int bps;
	std::vector<std::pair<const std::string*, int>> inputpairs{};
	try {
		checkImages(inputfiles, &width, &height, &depth, &channels, &bps, &inputpairs, skip_check, depth_hint);
	} catch(std::exception& e) {
		std::cerr<<argv[0]<<": Error while checking input images: "<<e.what();
		usage(argv[0], -1);
	}

	int64_t cachesize=cachesizef*1024*1024*1024;

	try {
		if(bps==16 && conv_x0>=conv_x1) {
			Slice2Cube<uint16_t> slice2cube{inputfiles, std::move(inputpairs), xres, yres, zres, width, height, depth, channels, bps, xsize, ysize, zsize, dsx, dsy, dsz, outdir, cachesize, nullptr, ncpu, resume, ds_only, ds_mip};
			slice2cube.printConversionInfo();
			if(!yes_set) {
				if(!get_confirmation())
					return 0;
			}
			slice2cube.start();
		} else {
			std::vector<uint8_t> xfunc{};
			if(conv_x0<conv_x1 && bps==16) {
				xfunc.resize(65536);
				for(size_t i=0; i<=conv_x0; i++)
					xfunc[i]=0;
				for(size_t i=conv_x1; i<65536; i++)
					xfunc[i]=255;
				for(size_t i=conv_x0+1; i<conv_x1; i++)
					xfunc[i]=lrint(255.0*(i-conv_x0)/(conv_x1-conv_x0));
			}
			Slice2Cube<uint8_t> slice2cube{inputfiles, std::move(inputpairs), xres, yres, zres, width, height, depth, channels, bps, xsize, ysize, zsize, dsx, dsy, dsz, outdir, cachesize, xfunc.size()>0?&xfunc[0]:nullptr, ncpu, resume, ds_only, ds_mip};
			slice2cube.printConversionInfo();
			if(!yes_set) {
				if(!get_confirmation())
					return 0;
			}
			slice2cube.start();
		}
	} catch(std::exception& e) {
		std::cerr<<argv[0]<<": Conversion error: "<<e.what();
		usage(argv[0], -1);
	}
	return 0;
}

